﻿// Copyright (c) Stéphane ANDRE. All Right Reserved.
// See the LICENSE file in the project root for more information.

using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
using MyNet.Wpf.Extensions;

namespace MyNet.Wpf.Parameters;

public static class DataGridAssist
{
    private static readonly Thickness DefaultCellPaddingThickness = new(8, 8, 8, 8);
    private static readonly Thickness DefaultColumnHeaderPadding = new(16, 10, 16, 10);

    public static readonly DependencyProperty ApplyDefaultStyleProperty = DependencyProperty.RegisterAttached(
            "ApplyDefaultStyle", typeof(bool), typeof(DataGridAssist), new PropertyMetadata(false, OnApplyDefaultStyleChanged));

    private static void OnApplyDefaultStyleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var grid = (DataGrid)d;
        if ((bool)e.NewValue)
        {
            if (grid.AutoGenerateColumns)
            {
                grid.AutoGeneratedColumns += Grid_AutoGeneratedColumns;
            }

            UpdateTextColumnStyles(grid);
            UpdateComboBoxColumnStyles(grid);
            UpdateCheckBoxColumnStyles(grid);
        }
        else
        {
            grid.AutoGeneratedColumns -= Grid_AutoGeneratedColumns;
        }
    }

    private static void Grid_AutoGeneratedColumns(object? sender, System.EventArgs e)
    {
        if (sender is not DataGrid grid) return;

        UpdateTextColumnStyles(grid);
        UpdateComboBoxColumnStyles(grid);
        UpdateCheckBoxColumnStyles(grid);
    }

    public static void SetApplyDefaultStyle(DependencyObject element, bool value)
        => element.SetValue(ApplyDefaultStyleProperty, value);

    public static bool GetApplyDefaultStyle(DependencyObject element)
        => (bool)element.GetValue(ApplyDefaultStyleProperty);

    public static readonly DependencyProperty TextColumnStyleProperty = DependencyProperty.RegisterAttached(
        "TextColumnStyle", typeof(Style), typeof(DataGridAssist), new PropertyMetadata(default(Style), OnTextColumnStyleChanged));

    private static void OnTextColumnStyleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var grid = (DataGrid)d;
        if (e.OldValue == null && e.NewValue != null)
        {
            UpdateTextColumnStyles(grid);
        }
    }

    public static void SetTextColumnStyle(DependencyObject element, Style value) => element.SetValue(TextColumnStyleProperty, value);

    public static Style GetTextColumnStyle(DependencyObject element) => (Style)element.GetValue(TextColumnStyleProperty);

    public static readonly DependencyProperty EditingTextColumnStyleProperty = DependencyProperty.RegisterAttached(
        "EditingTextColumnStyle", typeof(Style), typeof(DataGridAssist), new PropertyMetadata(default(Style), OnTextColumnStyleChanged));

    public static void SetEditingTextColumnStyle(DependencyObject element, Style value) => element.SetValue(EditingTextColumnStyleProperty, value);

    public static Style GetEditingTextColumnStyle(DependencyObject element) => (Style)element.GetValue(EditingTextColumnStyleProperty);

    private static void UpdateTextColumnStyles(DataGrid grid) => UpdateColumnStyles<DataGridTextColumn>(grid, GetTextColumnStyle(grid), GetEditingTextColumnStyle(grid));

    public static readonly DependencyProperty ComboBoxColumnStyleProperty = DependencyProperty.RegisterAttached(
        "ComboBoxColumnStyle", typeof(Style), typeof(DataGridAssist), new PropertyMetadata(default(Style), OnComboBoxColumnStyleChanged));

    private static void OnComboBoxColumnStyleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var grid = (DataGrid)d;
        if (e.OldValue == null && e.NewValue != null)
        {
            UpdateComboBoxColumnStyles(grid);
        }
    }

    public static void SetComboBoxColumnStyle(DependencyObject element, Style value) => element.SetValue(ComboBoxColumnStyleProperty, value);

    public static Style GetComboBoxColumnStyle(DependencyObject element) => (Style)element.GetValue(ComboBoxColumnStyleProperty);

    public static readonly DependencyProperty EditingComboBoxColumnStyleProperty = DependencyProperty.RegisterAttached(
        "EditingComboBoxColumnStyle", typeof(Style), typeof(DataGridAssist), new PropertyMetadata(default(Style), OnComboBoxColumnStyleChanged));

    public static void SetEditingComboBoxColumnStyle(DependencyObject element, Style value) => element.SetValue(EditingComboBoxColumnStyleProperty, value);

    public static Style GetEditingComboBoxColumnStyle(DependencyObject element) => (Style)element.GetValue(EditingComboBoxColumnStyleProperty);

    private static void UpdateComboBoxColumnStyles(DataGrid grid) => UpdateColumnStyles<DataGridComboBoxColumn>(grid, GetComboBoxColumnStyle(grid), GetEditingComboBoxColumnStyle(grid));

    public static readonly DependencyProperty CheckBoxColumnStyleProperty = DependencyProperty.RegisterAttached(
        "CheckBoxColumnStyle", typeof(Style), typeof(DataGridAssist), new PropertyMetadata(default(Style), OnCheckBoxColumnStyleChanged));

    private static void OnCheckBoxColumnStyleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var grid = (DataGrid)d;
        if (e.OldValue == null && e.NewValue != null)
        {
            UpdateCheckBoxColumnStyles(grid);
        }
    }

    public static void SetCheckBoxColumnStyle(DependencyObject element, Style value) => element.SetValue(CheckBoxColumnStyleProperty, value);

    public static Style GetCheckBoxColumnStyle(DependencyObject element) => (Style)element.GetValue(CheckBoxColumnStyleProperty);

    public static readonly DependencyProperty EditingCheckBoxColumnStyleProperty = DependencyProperty.RegisterAttached(
        "EditingCheckBoxColumnStyle", typeof(Style), typeof(DataGridAssist), new PropertyMetadata(default(Style), OnCheckBoxColumnStyleChanged));

    public static void SetEditingCheckBoxColumnStyle(DependencyObject element, Style value) => element.SetValue(EditingCheckBoxColumnStyleProperty, value);

    public static Style GetEditingCheckBoxColumnStyle(DependencyObject element) => (Style)element.GetValue(EditingCheckBoxColumnStyleProperty);

    private static void UpdateCheckBoxColumnStyles(DataGrid grid) => UpdateColumnStyles<DataGridCheckBoxColumn>(grid, GetCheckBoxColumnStyle(grid), GetEditingCheckBoxColumnStyle(grid));

    private static void UpdateColumnStyles<T>(DataGrid grid, Style? elementStyle, Style? editingStyle) where T : DataGridColumn
    {
        foreach (var column in grid.Columns.OfType<T>().Where(x => x.GetType() == typeof(T)))
        {
            if (elementStyle != null)
            {
                if (column is DataGridBoundColumn boundColumn)
                    boundColumn.ElementStyle = elementStyle;
                else if (column is DataGridComboBoxColumn comboBoxColumn)
                    comboBoxColumn.ElementStyle = elementStyle;
            }

            if (editingStyle != null)
            {
                if (column is DataGridBoundColumn boundColumn)
                    boundColumn.EditingElementStyle = editingStyle;
                else if (column is DataGridComboBoxColumn comboBoxColumn)
                    comboBoxColumn.EditingElementStyle = editingStyle;
            }
        }
    }

    #region AttachedProperty : CellPaddingProperty
    public static readonly DependencyProperty CellPaddingProperty
        = DependencyProperty.RegisterAttached("CellPadding", typeof(Thickness), typeof(DataGridAssist),
            new FrameworkPropertyMetadata(DefaultCellPaddingThickness, FrameworkPropertyMetadataOptions.Inherits));

    public static Thickness GetCellPadding(DataGrid element)
        => (Thickness)element.GetValue(CellPaddingProperty);
    public static void SetCellPadding(DataGrid element, Thickness value)
        => element.SetValue(CellPaddingProperty, value);
    #endregion

    #region AttachedProperty : SelectedCellBorderBrushProperty
    public static readonly DependencyProperty SelectedCellBorderBrushProperty
        = DependencyProperty.RegisterAttached("SelectedCellBorderBrush", typeof(Brush), typeof(DataGridAssist),
            new PropertyMetadata(null));

    public static Brush GetSelectedCellBorderBrush(DataGrid element)
        => (Brush)element.GetValue(SelectedCellBorderBrushProperty);
    public static void SetSelectedCellBorderBrush(DataGrid element, Brush value)
        => element.SetValue(SelectedCellBorderBrushProperty, value);
    #endregion

    #region AttachedProperty : ColumnHeaderPaddingProperty
    public static readonly DependencyProperty ColumnHeaderPaddingProperty
        = DependencyProperty.RegisterAttached("ColumnHeaderPadding", typeof(Thickness), typeof(DataGridAssist),
            new FrameworkPropertyMetadata(DefaultColumnHeaderPadding, FrameworkPropertyMetadataOptions.Inherits));

    public static Thickness GetColumnHeaderPadding(DataGrid element)
        => (Thickness)element.GetValue(ColumnHeaderPaddingProperty);
    public static void SetColumnHeaderPadding(DependencyObject element, Thickness value)
        => element.SetValue(ColumnHeaderPaddingProperty, value);
    #endregion

    #region AttachedProperty : EnableEditBoxAssistProperty
    public static readonly DependencyProperty EnableEditBoxAssistProperty
        = DependencyProperty.RegisterAttached("EnableEditBoxAssist", typeof(bool), typeof(DataGridAssist),
            new PropertyMetadata(default(bool), EnableEditBoxAssistPropertyChangedCallback));

    public static bool GetEnableEditBoxAssist(DataGrid element)
        => (bool)element.GetValue(EnableEditBoxAssistProperty);
    public static void SetEnableEditBoxAssist(DataGrid element, bool value)
        => element.SetValue(EnableEditBoxAssistProperty, value);

    private static void EnableEditBoxAssistPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var dataGrid = (DataGrid)d;
        var enableCheckBoxAssist = (bool)e.NewValue;

        if (enableCheckBoxAssist)
        {
            // Register for bubbling events from cells, even when the cell handles them (thus the 'true' parameter)
            dataGrid.AddHandler(UIElement.MouseLeftButtonDownEvent, (RoutedEventHandler)OnMouseLeftButtonDown, true);
            dataGrid.PreviewKeyDown += EditOnSpacebarPress;
        }
        else
        {
            dataGrid.RemoveHandler(UIElement.MouseLeftButtonDownEvent, (RoutedEventHandler)OnMouseLeftButtonDown);
            dataGrid.PreviewKeyDown -= EditOnSpacebarPress;
        }
    }

    // This relay is only needed because the UIElement.AddHandler() has strict requirements for the signature of the passed Delegate
    private static void OnMouseLeftButtonDown(object sender, RoutedEventArgs e) => AllowDirectEditWithoutFocus(sender, (MouseButtonEventArgs)e);

    #endregion

    private static void EditOnSpacebarPress(object sender, KeyEventArgs e)
    {
        var dataGrid = (DataGrid)sender;
        if (e.Key == Key.Space && e.OriginalSource is DataGridCell { IsReadOnly: false } cell)
        {
            switch (cell.Column)
            {
                case DataGridComboBoxColumn:
                case DataGridTextColumn:
                    dataGrid.BeginEdit();
                    e.Handled = true;
                    break;
            }
        }
    }

    /// <summary>
    /// Allows editing of components inside of a data grid cell with a single left click.
    /// </summary>
    private static void AllowDirectEditWithoutFocus(object sender, MouseButtonEventArgs mouseArgs)
    {
        var originalSource = (DependencyObject)mouseArgs.OriginalSource;
        var dataGridCell = originalSource
            .GetVisualAncestry()
            .OfType<DataGridCell>()
            .FirstOrDefault();

        // Readonly has to be handled as the pass-through ignores the
        // cell and interacts directly with the content
        if (dataGridCell?.IsReadOnly ?? true)
        {
            return;
        }

        if (dataGridCell.Content is UIElement element)
        {
            var dataGrid = (DataGrid)sender;
            // If it is a DataGridTemplateColumn we want the
            // click to be handled naturally by the control
            if (dataGridCell.Column.GetType() == typeof(DataGridTemplateColumn))
            {
                return;
            }
            if (dataGridCell.IsEditing)
            {
                // If the cell is already being edited, we don't want to (re)start editing
                return;
            }
            //NB: Issue 2852 - Don't handle events from nested data grids
            var parentDataGrid = dataGridCell
                .GetVisualAncestry()
                .OfType<DataGrid>()
                .FirstOrDefault();
            if (parentDataGrid != dataGrid)
            {
                return;
            }

            dataGrid.CurrentCell = new DataGridCellInfo(dataGridCell);
            dataGrid.BeginEdit();

            switch (dataGridCell.Content)
            {
                case TextBoxBase textBox:
                    {
                        // Send a 'left-click' routed event to the TextBox to place the I-beam at the position of the mouse cursor
                        var mouseDownEvent = new MouseButtonEventArgs(mouseArgs.MouseDevice, mouseArgs.Timestamp, mouseArgs.ChangedButton)
                        {
                            RoutedEvent = Mouse.MouseDownEvent,
                            Source = mouseArgs.Source
                        };
                        textBox.RaiseEvent(mouseDownEvent);
                        break;
                    }

                case ToggleButton toggleButton:
                    {
                        // Check if the cursor actually hit the checkbox and not just the cell
                        var mousePosition = mouseArgs.GetPosition(element);
                        var elementHitBox = new Rect(element.RenderSize);
                        if (elementHitBox.Contains(mousePosition))
                        {
                            // Send a 'left click' routed command to the toggleButton to toggle the state
                            var newMouseEvent = new MouseButtonEventArgs(mouseArgs.MouseDevice, mouseArgs.Timestamp, mouseArgs.ChangedButton)
                            {
                                RoutedEvent = Mouse.MouseDownEvent,
                                Source = dataGrid
                            };

                            toggleButton.RaiseEvent(newMouseEvent);
                        }
                        break;
                    }

                // Open the dropdown explicitly. Left clicking is not
                // viable, as it would edit the text and not open the
                // dropdown
                case ComboBox comboBox:
                    {
                        comboBox.IsDropDownOpen = true;
                        break;
                    }
            }
        }
    }
}
